-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local singleton = require 'mc.singleton'
local log = require 'mc.logging'
local consts = require 'constants'
local skynet = require 'skynet'
local client_ok, cnc_client_instance = pcall(require, 'mc.cnc.client')
local tcp_mgmt_instance = require 'tcp.tcp_mgmt'
local vos = require 'utils.vos'

local module = class{}

local tcp_destination_fmt = 'tcp://peer:ipv4:%s:%s:1'

function module:ctor()
    self.mdb_obj_list = {}
    self.cnc_client_list = {}
    self.destination_addr_list = {}
    self.identifier = nil
    self.sync_filter_list = {}
end

-- 用于统计发送失败次数
local send_count = {
    total = {},
    failed = {}
}

local function add_send_count(is_success, address)
    send_count.total[address] = send_count.total[address] and send_count.total[address] + 1 or 1
    if is_success then
        send_count.failed[address] = 0  -- 中途有同步成功则数据清0
        return
    end
    if not send_count.failed[address] then
        send_count.failed[address] = 1
        return
    end
    send_count.failed[address] = send_count.failed[address] + 1
    if send_count.failed[address] > 10 then  -- 每连续失败10次输出一次日志
        log:warn('too many synchronization failures to [%s]', address)
        send_count.failed[address] = 0
    end
end

function module:destroy_cnc_client(object_name)
    if self.cnc_client_list[object_name] then
        self.cnc_client_list[object_name]:destroy()
        self.cnc_client_list[object_name] = nil
    end
end

-- 根据使能状态更新所有客户端
function module:update_cnc_client()
    -- 只有既开启TCP同步使能又时发送者才需要创建客户端
    if tcp_mgmt_instance:get_instance():get_sync_role() == consts.DATASYNC_ROLE_SENDER then
        if next(self.cnc_client_list) then
            return
        end
        for object_name, _ in pairs(self.destination_addr_list) do
            self:create_cnc_client(object_name)
        end
        return
    end
    --  如果本来就没有客户端就可以不用禁用
    if not next(self.cnc_client_list) then
        return
    end

    for _, client in pairs(self.cnc_client_list) do
        client:destroy()
    end
    self.cnc_client_list = {}
end

function module:init_sig(tcp_enabled_sig, identifier)
    -- 注册回调
    tcp_enabled_sig:on(function()
        self:update_cnc_client()
    end)

    self.identifier = identifier
end

function module:start_listen_task(mdb_object, tcp_mgmt_object, object_name)
    mdb_object.property_changed:on(function(name, _, _)
        if name ~= 'Address' and name ~= 'Port' then
            return
        end
        self.destination_addr_list[object_name] = string.format(tcp_destination_fmt, mdb_object.Address, mdb_object.Port)
        log:notice('destination config address [%s] change to [%s]', object_name, self.destination_addr_list[object_name])
        -- 地址变更时检测是否需要创建客户端，否则更新地址即可
        if tcp_mgmt_object:get_sync_role() == consts.DATASYNC_ROLE_SENDER then
            self:destroy_cnc_client(object_name)
            self:create_cnc_client(object_name)
        end
    end)
end

function module:register(mdb_object, tcp_enabled_sig)
    tcp_enabled_sig:on(function()
        self:update_cnc_client()
    end)
    local object_name = mdb_object.name
    self.mdb_obj_list[object_name] = mdb_object
    self.sync_filter_list[object_name] = mdb_object.DestinationFilter
    local tcp_mgmt_object = tcp_mgmt_instance:get_instance()
    -- 创建对于IP地址的监听，发生变更后需要重新建立服务端
    self:start_listen_task(mdb_object, tcp_mgmt_object, object_name)
    self.destination_addr_list[object_name] = string.format(tcp_destination_fmt, mdb_object.Address, mdb_object.Port)
    log:notice('init destination config [%s] address [%s]', object_name, self.destination_addr_list[object_name])
    -- TCP同步禁用或者当前节点是接收者时不创建客户端
    if tcp_mgmt_object:get_sync_role() == consts.DATASYNC_ROLE_SENDER then
        skynet.fork_once(function ()
            self:create_cnc_client(object_name)
        end)
    end
end

function module:create_cnc_client(object_name)
    if not client_ok then
        log:info('require service failed, err %s', cnc_client_instance)
        return
    end
    local ok, err
    local try_count = 0  --  统计重试次数（默认0）
    while not ok and try_count ~= 60 do  -- 重试60次（10分钟）
        ok, err = pcall(function()
            self.cnc_client_list[object_name] = cnc_client_instance.new(self.destination_addr_list[object_name])
        end)
        if ok then
            break
        end
        if try_count % 10 == 0 then  -- 取10的余数，等于0打印日志，即每重试10次打印一次
            log:error('create cnc client failed, %s', err)
        end
        try_count = try_count + 1
        skynet.sleep(1000)  -- 间隔10秒重试，服务刚启动时可能失败（1000表征10秒）
    end
    if ok then
        log:notice('create cnc client success, try_count: [%s]', try_count)
    end
end

-- 如果3次都失败可能是远端节点已重启等原因，旧的客户端已失效，需要重新创建
function module:send_with_try(client, method, datas, name)
    local count = 3
    local ok , err
    while count > 0 do
        ok, err = pcall(function()
            client:call_remote_class_method('bmc.kepler.bmc_datasync',
                '', '', method, '', '', datas)
        end)
        if not ok then
            log:info('send data failed [%s]', err)
            count = count - 1
        else
            return true
        end
    end
    -- 尝试重新拉起客户端
    log:info('try to reset tcp connect to [%s]', self.destination_addr_list[name])
    client:destroy()
    self.cnc_client_list[name] = nil
    ok, err = pcall(function()
        self.cnc_client_list[name] = cnc_client_instance.new(self.destination_addr_list[name])
    end)
    -- 创建成功后再失败则不需要重试，表明确实通信异常
    if ok then
        ok, err = pcall(function()
            client:call_remote_class_method('bmc.kepler.bmc_datasync',
                '', '', method, '', {}, datas)
        end)
    end
    return ok, err
end

function module:send_msg(datas, send_type)
    -- 判断TCPMgmt是否注册
    if not self.identifier then
        self.identifier = tcp_mgmt_instance:get_instance().identifier
    end
    if not self.identifier then
        log:info('send datas failed, not have identifier')
        return
    end
    -- 如果没有客户端需要尝试拉起
    if not next(self.cnc_client_list) then
        self:update_cnc_client()
    end
    if not next(self.cnc_client_list) then
        log:info('send datas failed, not exist client')
        return
    end
    if send_type == 'property' then
        self:send_sync_property(datas)
    elseif send_type == 'file' then
        self:send_sync_file(datas)
    end
end

function module:send_sync_property(datas)
    for name, client in pairs(self.cnc_client_list) do
        if self.sync_filter_list[name] == consts.DATASYNC_USAGE_FILE then
            goto continue
        end
        datas['Identifier'] = self.identifier
        local ok, err = self:send_with_try(client, 'UpdateProperty', datas, name)
        if not ok then
            log:info('send sync data to [%s] failed, [%s]', self.destination_addr_list[name], err)
        end
        add_send_count(ok, self.destination_addr_list[name])
        ::continue::
    end
end

function module:send_sync_file(datas)
    for name, client in pairs(self.cnc_client_list) do
        if self.sync_filter_list[name] == consts.DATASYNC_USAGE_PROPERTY then
            goto continue
        end
        datas['Identifier'] = self.identifier
        local ok, err = self:send_with_try(client, 'UpdateFile', datas, name)
        if not ok then
            log:info('send sync data to [%s] failed, [%s]', self.destination_addr_list[name], err)
        end
        add_send_count(ok, self.destination_addr_list[name])
        ::continue::
    end
end

-- 属性同步当前未分帧传输，所以此函数远端无操作
-- 如果涉及分帧，需要停止数据接收
function module:reboot_process()
    if not next(self.cnc_client_list) then
        log:info('no need reboot process, not exist client')
        return
    end
    local process_time, reboot_res = nil, {timeout = 0, failed = 0}
    local datas = {Identifier = self.identifier}
    for name, client in pairs(self.cnc_client_list) do
        process_time = vos.vos_tick_get()
        local ok, err = self:send_with_try(client, 'RebootProcess', datas, name)
        add_send_count(ok, self.destination_addr_list[name])
        if not ok then
            log:error('reboot process to [%s] failed, [%s]', self.destination_addr_list[name], err)
            reboot_res.failed = reboot_res.failed + 1
        elseif vos.vos_tick_get() - process_time > 10 then  -- 单个节点耗时基线10秒
            log:warn('reboot process [%s] timeout', self.destination_addr_list[name])
            reboot_res.timeout = reboot_res.timeout + 1
        end
    end
    return string.format('timeout count: %s, failed count %s', reboot_res.timeout, reboot_res.failed)
end

return singleton(module)